package com.xcity.game.proxy.utils;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.xcity.db.entity.Player;
import com.xcity.game.proxy.DynamicProxy;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import javassist.CtNewMethod;

/**
 * 动态代理类生成工具
 * @author yang.li
 *
 */
public class DynamicProxyUtils {
	
	private static Map<Class<?>, ProxyClass> proxyClasses = new HashMap<>(); // cache all proxy classes
	private static Constructor<?> playerProxyClassConstructor;
	
	static class ProxyClass {
		CtClass cclass;
		Class<?> javaClass;
		ProxyClass(CtClass cclass) {
			this.cclass = cclass;
			try {
				this.javaClass = cclass.toClass(); // try load
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
	
	static {
		// cache some classes
		try {
			ProxyClass pc = getProxyClass(Player.class);
			playerProxyClassConstructor = pc.javaClass.getConstructor(Player.class);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public static void init() {
		// do nothing, just load the class
	}
	
	public static <T> T newProxy(T object) {
		try {
			Class<?> javaClass = object.getClass();
			ProxyClass pc = getProxyClass(javaClass);
			Constructor<?> conn = pc.javaClass.getConstructor(javaClass);
			return (T) conn.newInstance(object);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return object; // can't make the proxy instance
	}
	
	public static Player newProxy(Player player) {
		try {
			return (Player) playerProxyClassConstructor.newInstance(player);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return player;
	}
	
	/*package*/ static ProxyClass getProxyClass(Class<?> javaClass) throws Exception {
		ProxyClass pc = proxyClasses.get(javaClass);
		if (pc == null) {
			CtClass cclass = makeProxyClass(javaClass);
			proxyClasses.put(javaClass, pc = new ProxyClass(cclass));
		}
		return pc;
	}
	
	private static CtClass makeProxyClass(Class<?> javaClass) throws Exception {
		Constructor<?> defaultConn = javaClass.getConstructor();
		if (defaultConn == null) {
			throw new UnsupportedOperationException("can't make the proxy class for " + javaClass + ", must define a constructor without any parameters.");
		}
		ClassPool cpool = ClassPool.getDefault();
		CtClass nativeCtClass = cpool.get(javaClass.getName());
		CtClass declaring = createProxyCtClass(javaClass);
		// make the fields
		makeField(declaring, cpool.get("boolean"), "dirty");
		CtField realobjfield = makeField(declaring, nativeCtClass, "real_obj");
		// scan the methods
		Set<MethodInfo> methodInfos = scanMethods(javaClass, declaring);
		// make the constructor
		makeConstructor(declaring, realobjfield, methodInfos);
		// implements the interface's methods
		makeInterfaceMethods(declaring, realobjfield);
		// make the proxy methods
		for (MethodInfo mi : methodInfos) {
			makeMethod(declaring, realobjfield, mi);
		}
//		IOUtils.write(new File("C:/Users/yang.li/Desktop/server_release/asm/" + declaring.getSimpleName() + ".class"), declaring.toBytecode());
		return declaring;
	}
	
	private static CtClass createProxyCtClass(Class<?> javaClass) throws Exception {
		ClassPool cpool = ClassPool.getDefault();
		String classname = javaClass.getName();
		CtClass supercclass = cpool.get(classname);
		CtClass cclass = cpool.makeClass(classname + "_Proxy", supercclass);
		cclass.addInterface(cpool.get(DynamicProxy.class.getName()));
		return cclass;
	}
	
	private static Set<MethodInfo> scanMethods(Class<?> javaClass, CtClass proxyClass) {
		Set<MethodInfo> set = new LinkedHashSet<>();
		scanMethods(javaClass, proxyClass, set);
		return set;
	}
	
	private static void scanMethods(Class<?> javaClass, CtClass proxyClass, Set<MethodInfo> methodInfos) {
		Method[] methods = javaClass.getDeclaredMethods();
		for (Method m : methods) {
			int mod = m.getModifiers();
			if (!Modifier.isPrivate(mod) && !Modifier.isFinal(mod) && !Modifier.isStatic(mod)) {
				MethodInfo mi = new MethodInfo(m);
				if (!methodInfos.contains(mi)) {
					mi.init(proxyClass);
					methodInfos.add(mi);
				}
			}
		}
		javaClass = javaClass.getSuperclass();
		if (javaClass != Object.class) {
			scanMethods(javaClass, proxyClass, methodInfos);
		}
	}
	
	/*package*/ static CtField makeField(CtClass declaring, CtClass fieldClass, String fieldName) throws Exception {
		CtField cfield = new CtField(fieldClass, fieldName, declaring);
		cfield.setModifiers(Modifier.PRIVATE);
		declaring.addField(cfield);
		return cfield;
	}
	
	private static void makeConstructor(CtClass declaring, CtField parameterField, Set<MethodInfo> methodInfos) throws Exception {
		CtConstructor ccon = new CtConstructor(new CtClass[] { parameterField.getType() }, declaring);
		StringBuilder code = new StringBuilder(64);
		code.append('{');
		// the parameter
		code.append("this.").append(parameterField.getName()).append(" = $1;");
		// the proxy fields
		for (MethodInfo mi : methodInfos) {
			CtField proxiedField = mi.cfield;
			if (proxiedField != null) {
				// this.proxy_field = new ProxyFieldClass($1.getXXX());
				code.append("this.").append(proxiedField.getName())
					.append(" = new ").append(proxiedField.getType().getName()).append("($1.").append(mi.method.getName()).append("());");
			}
		}
		code.append('}');
		ccon.setBody(code.toString());
		declaring.addConstructor(ccon);
	}
	
	private static void makeInterfaceMethods(CtClass declaring, CtField realobjfield) throws Exception {
		List<CtField> proxyFields = getProxyFields(declaring); // 获取所有代理字段
		// DynamicProxy#isDirty
		StringBuilder code = new StringBuilder("public boolean isDirty() { if (this.dirty) { return true; }");
		for (int i = 0; i < proxyFields.size(); i++) {
			CtField cfield = proxyFields.get(i);
			code.append("if (this.").append(cfield.getName()).append(".isDirty()) { return true; }");
		}
		code.append("return false; }");
		declaring.addMethod(CtNewMethod.make(code.toString(), declaring));
		// DynamicProxy#setDirty
		code.setLength(0);
		code.append("public void setDirty(boolean dirty) {");
		code.append("this.dirty = $1;");
		for (int i = 0; i < proxyFields.size(); i++) {
			CtField cfield = proxyFields.get(i);
			code.append("this.").append(cfield.getName()).append(".setDirty($1);");
		}
		code.append('}');
		declaring.addMethod(CtNewMethod.make(code.toString(), declaring));
		// MutableProxy#getRealObject
		code.setLength(0);
		code.append("public Object getRealObject() { return this.").append(realobjfield.getName()).append("; }");
		declaring.addMethod(CtNewMethod.make(code.toString(), declaring));
	}
	
	private static void makeMethod(CtClass declaring, CtField realobjfield, MethodInfo mi) throws Exception {
		StringBuilder code = new StringBuilder(1024);
		// visibility->public/protected/package
		makeMethodVisibilityCode(mi, code);
		// return type
		makeMethodReturnTypeCode(mi, code);
		// method name
		makeMethodNameCode(mi, code);
		// parameters
		makeMethodParametersCode(mi, code);
		// body
		makeMethodBodyCode(mi, code, realobjfield);
		declaring.addMethod(CtNewMethod.make(code.toString(), declaring));
	}
	
	private static void makeMethodVisibilityCode(MethodInfo mi, StringBuilder code) {
		int mod = mi.method.getModifiers();
		if (Modifier.isPublic(mod)) {
			code.append("public ");
		} else if (Modifier.isProtected(mod)) {
			code.append("protected ");
		}
	}
	
	private static void makeMethodReturnTypeCode(MethodInfo mi, StringBuilder code) {
		Class<?> returnType = mi.method.getReturnType();
		if (returnType.isArray()) {
			code.append(returnType.getComponentType().getName()).append("[] ");
		} else {
			code.append(returnType.getName()).append(' ');
		}
	}
	
	private static void makeMethodNameCode(MethodInfo mi, StringBuilder code) {
		code.append(mi.method.getName());
	}
	
	private static void makeMethodParametersCode(MethodInfo mi, StringBuilder code) {
		code.append(" ( ");
		Class<?>[] paramTypes = mi.method.getParameterTypes();
		for (int i = 0, len = paramTypes.length; i < len; i++) {
			if (i != 0) {
				code.append(", ");
			}
			Class<?> paramType = paramTypes[i];
			if (paramType.isArray()) {
				code.append(paramType.getComponentType().getName()).append("[] _p").append(i);
			} else {
				code.append(paramType.getName()).append(" _p").append(i);
			}
		}
		code.append(" ) ");
	}
	
	private static void makeMethodBodyCode(MethodInfo mi, StringBuilder code, CtField realobjfield) {
		code.append("{");
		Method method = mi.method;
		Class<?> returnType = method.getReturnType();
		if (mi.annotation != null && mi.annotation.makeDirty()) {
			// 调用该方法会导致脏数据
			code.append("this.dirty = true;");
		}
		if (returnType != void.class) {
			code.append("return ");
		}
		if (mi.cfield != null) { // 代理字段（一定是getter方法）
			code.append("this.").append(mi.cfield.getName()).append(";}");
			return;
		}
		
		code.append("this.").append(realobjfield.getName()).append('.').append(method.getName()).append('(');
		Class<?>[] paramTypes = method.getParameterTypes();
		for (int j = 0, len = paramTypes.length; j < len; j++) {
			if (j != 0) {
				code.append(", ");
			}
			code.append('$').append(j + 1);
		}
		code.append(");}");
	}
	
	private static List<CtField> getProxyFields(CtClass declaring) throws Exception {
		CtField[] cfields = declaring.getDeclaredFields();
		CtClass dynamicProxyInterface = ClassPool.getDefault().get(DynamicProxy.class.getName());
		List<CtField> proxyFields = new ArrayList<CtField>();
		for (CtField cfield : cfields) {
			CtClass[] interfaces = cfield.getType().getInterfaces();
			if (interfaces != null && interfaces.length > 0) {
				for (CtClass cc : interfaces) {
					if (cc == dynamicProxyInterface) {
						proxyFields.add(cfield);
					}
				}
			}
		}
		return proxyFields;
	}
	
}
